package com.pixelcan.inkpageindicator;

import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorGroup;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.colors.RgbColor;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.PageSlider;
import ohos.agp.components.element.ShapeElement;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.Path;
import ohos.agp.utils.Color;
import ohos.agp.utils.Point;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * InkPageIndicator
 */
public class InkPageIndicator extends Component implements PageSlider.PageChangedListener,
        Component.DrawTask, Component.EstimateSizeListener {

    // defaults
    private static final int DEFAULT_DOT_SIZE = 8;                      // dp
    private static final int DEFAULT_GAP = 12;                          // dp
    private static final int DEFAULT_ANIM_DURATION = 400;               // ms
    private static final int DEFAULT_UNSELECTED_COLOUR = 0x80ffffff;    // 50% white   0x80ffffff
    private static final int DEFAULT_SELECTED_COLOUR = 0xffffffff;      // 100% white  0xffffffff

    // constants
    private static final float INVALID_FRACTION = -1f;
    private static final float MINIMAL_REVEAL = 0.01f;

    // configurable attributes
    private final int dotDiameter;
    private int gap;
    private long animDuration;
    private int unselectedColour;
    private int selectedColour;

    // derived from attributes
    private double dotRadius;
    private double halfDotRadius;
    private long animHalfDuration;
    private float dotTopY;
    private float dotCenterY;
    private float dotBottomY;

    // ViewPager
    private PageSlider viewPager; //PageSlider

    // state
    private int pageCount;
    private int currentPage;
    private int previousPage;
    private double selectedDotX;
    private boolean selectedDotInPosition;
    private float[] dotCenterX;
    private float[] joiningFractions;
    private float retreatingJoinX1;
    private float retreatingJoinX2;
    private float[] dotRevealFractions;
    private boolean isAttachedToWindow;
    private boolean pageChanging;

    // drawing
    private final Paint unselectedPaint;
    private final Paint selectedPaint;
    private Path combinedUnselectedPath;
    private final Path unselectedDotPath;
    private final Path unselectedDotLeftPath;
    private final Path unselectedDotRightPath;
    private final RectFloat rectF;//RectFloat

    // animation
    private AnimatorValue moveAnimation;//AnimatorValue
    private AnimatorGroup joiningAnimationSet;//AnimatorGroup
    private PendingRetreatAnimator retreatAnimation;//撤退动画
    private PendingRevealAnimator[] revealAnimations;//显示点动画

    // working values for beziers
    private double endX1;
    private double endY1;
    private double endX2;
    private double endY2;
    private float controlX1;
    private float controlY1;
    private float controlX2;
    private float controlY2;

    /**
     * InkPageIndicator
     *
     * @param context
     * @param attrs
     */
    public InkPageIndicator(Context context, AttrSet attrs) {
        super(context, attrs);

        LogUtil.setPrintStatus(false);
        dotDiameter = AttrUtils.getInteger(context, attrs, "ipi_dotDiameter", DEFAULT_DOT_SIZE);
        LogUtil.loge("dotDiameter:" + dotDiameter);
        dotRadius = (float) dotDiameter / 2;
        LogUtil.loge("dotRadius==" + dotRadius);
        halfDotRadius = (float) (dotRadius / 2);
        gap = AttrUtils.getInteger(context, attrs, "ipi_dotGap", DEFAULT_GAP);

        animDuration = (long) AttrUtils.getInteger(context, attrs, "ipi_animationDuration", DEFAULT_ANIM_DURATION);
        animHalfDuration = animDuration / 2;
        unselectedColour = AttrUtils.getColor(attrs, "ipi_pageIndicatorColor", DEFAULT_UNSELECTED_COLOUR);
        selectedColour = AttrUtils.getColor(attrs, "ipi_currentPageIndicatorColor", DEFAULT_SELECTED_COLOUR);

        unselectedPaint = new Paint();//Paint.ANTI_ALIAS_FLAG
        unselectedPaint.setStrokeJoin(Paint.Join.ROUND_JOIN);
        unselectedPaint.setStrokeCap(Paint.StrokeCap.ROUND_CAP);
        unselectedPaint.setStyle(Paint.Style.FILL_STYLE);
        unselectedPaint.setAntiAlias(true);
        unselectedPaint.setColor(new Color(unselectedColour));

        selectedPaint = new Paint();//Paint.ANTI_ALIAS_FLAG
        selectedPaint.setStrokeJoin(Paint.Join.ROUND_JOIN);
        selectedPaint.setStrokeCap(Paint.StrokeCap.ROUND_CAP);
        selectedPaint.setStyle(Paint.Style.FILL_STYLE);
        selectedPaint.setAntiAlias(true);
        selectedPaint.setColor(new Color(selectedColour));

        // create paths & rect now – reuse & rewind later
        combinedUnselectedPath = new Path();
        unselectedDotPath = new Path();
        unselectedDotLeftPath = new Path();
        unselectedDotRightPath = new Path();
        rectF = new RectFloat();

//        ShapeElement se = new ShapeElement();
//        se.setRgbColor(new RgbColor(220,236,158));
//        setBackground(se);

        setEstimateSizeListener(this);

        setBindStateChangedListener(new BindStateChangedListener() {
            @Override
            public void onComponentBoundToWindow(Component component) {
                isAttachedToWindow = true;

                addDrawTask(InkPageIndicator.this::onDraw);
            }

            @Override
            public void onComponentUnboundFromWindow(Component component) {
                isAttachedToWindow = false;

            }
        });

    }

    /**
     * viewPager
     *
     * @param viewPager
     */
    public void setViewPager(PageSlider viewPager) {
        this.viewPager = viewPager;
        viewPager.addPageChangedListener(this);

        setPageCount(viewPager.getProvider().getCount());
        setCurrentPageImmediate();
    }


    private Float floatToSubtract(float f1, float f2) {
        BigDecimal b1 = new BigDecimal(Float.toString(f1));
        BigDecimal b2 = new BigDecimal(Float.toString(f2));

        DecimalFormat df = new DecimalFormat("0.00");
        return Float.valueOf(df.format(b1.subtract(b2)));
    }

    private boolean isNext = false;//是否滑动到下一页 预判动作
    private boolean isPageChosenStatus = false;//滑动状态 为true表示已选中   为兼容调用onPageChosen后positionOffsetPixels返回负数问题
    private DecimalFormat df = new DecimalFormat("0.00");
    private int isSlidingStateChanged = 0;//页面划定状态，用于兼容 positionOffset 滑动一点点后松开返回负数情况

    @Override
    public void onPageSliding(int position, float positionOffset, int positionOffsetPixels) {
        //positionOffsetPixels  下标从0->1  为正数   1->0  为负数  API 5 调用onPageChosen后开始返回负数
        positionOffset = Float.valueOf(df.format(positionOffset));
        LogUtil.loge("onPageSliding==进度原始数据==position=" + position + "===positionOffset=" + positionOffset + "=positionOffsetPixels=" + positionOffsetPixels + "=isPageChosenStatus=" + isPageChosenStatus + "=onPageSlideStateChanged=" + isSlidingStateChanged);//+"=onPageSlideStateChanged="+isSlidingStateChanged

        if (isAttachedToWindow) {
            float fraction = 0f;
            if (!isPageChosenStatus) {
                if (positionOffsetPixels != 0) {
                    if (positionOffsetPixels > 0) {// 下一页
                        isNext = true;
                    } else {// 上一页
                        isNext = false;
                    }
                }
            }

            if (isNext) {
                fraction = positionOffset;//下一页
            } else {
                fraction = floatToSubtract(1f, positionOffset);//上一页
                position = position - 1 > 0 ? position - 1 : 0;
            }

            LogUtil.loge("onPageSliding==进度转换后====fraction=" + fraction + "==isNext=" + isNext + "=pageChanging=" + pageChanging + "=onPageSlideStateChanged=" + isSlidingStateChanged);//+"=isPageChosenStatus="+ isPageChosenStatus
//            if(isSlidingStateChanged == 2 && !isPageChosenStatus){//&& (fraction != 0f) && (fraction != 1f)
//                fraction =  floatToSubtract(1f,fraction);
//            }
            LogUtil.loge("onPageSliding==进度转换后333=fraction=" + fraction + "==isNext=" + isNext + "=pageChanging=" + pageChanging + "=onPageSlideStateChanged=" + isSlidingStateChanged + "=isPageChosenStatus=" + isPageChosenStatus + "=currentPage=" + currentPage);//+"=isPageChosenStatus="+ isPageChosenStatus


            int currentPosition = pageChanging ? previousPage : currentPage;
//            LogUtil.loge("onPageScrolled","onPageSliding==111==currentPosition="+currentPosition+"=pageChanging="+pageChanging+"==previousPage=="+previousPage+"=currentPage="+currentPage);

            // when swiping from #2 to #1 ViewPager reports position as 1 and a descending offset
            //当从#2滑动到#1时，ViewPager报告位置为1和递减偏移量
            // need to convert this into our left-dot-based 'coordinate space'
            //需要将其转换为基于左点的“坐标空间”
            int leftDotPosition = position;

            LogUtil.loge("onPageSliding==进度11==leftDotPosition=" + leftDotPosition + "=position=" + position + "=positionOffset=" + positionOffset + "=fraction=" + fraction + "=currentPosition=" + currentPosition);
            if (currentPosition != position) {
                fraction = floatToSubtract(1f, fraction);//positionOffset

                LogUtil.loge("onPageSliding==进度111==fraction=" + fraction);
                // if user scrolls completely to next page then the position param updates to that
                //如果用户完全滚动到下一页，那么位置参数将更新到该页
                // new page but we're not ready to switch our 'current' page yet so adjust for that
                //新的页面，但我们还没有准备好切换我们的'当前'页，所以调整
                if (fraction == 1f) {
                    leftDotPosition = Math.min(currentPosition, position);
                }
            }

            LogUtil.loge("onPageSliding==进度222==" + "=leftDotPosition=" + leftDotPosition + "==fraction=" + fraction + "=currentPosition=" + currentPosition);
            setJoiningFraction(leftDotPosition, fraction);
        }
    }

    //选择新页面时回调。
    @Override
    public void onPageChosen(int position) {
        isPageChosenStatus = true;
        LogUtil.loge("onPageChosen==>" + isAttachedToWindow + "=position=" + position);
        if (isAttachedToWindow) {
            // this is the main event we're interested in!
            setSelectedPage(position);
        } else {
            // when not attached, don't animate the move, just store immediately
            setCurrentPageImmediate();
        }
    }

    //0、1或2，分别表示页面处于空闲、拖动或滑动状态。
    @Override
    public void onPageSlideStateChanged(int state) {
        LogUtil.loge("===onPageSlideStateChanged=============================================================================state==" + state);
        isSlidingStateChanged = state;
        if (state == 0) {
            isPageChosenStatus = false;
            isNext = false;

        }
    }


    private void setPageCount(int pages) {
        pageCount = pages;
        resetState();
        postLayout();
    }

    //计算出的位置
    private void calculateDotPositions(int width, int height) {
        int left = getPaddingLeft();
        int top = getPaddingTop();
        int right = width - getPaddingRight();

        int requiredWidth = getRequiredWidth();
        double startLeft = (left + ((right - left - requiredWidth) / 2) + dotRadius);

        dotCenterX = new float[pageCount];
        for (int i = 0; i < pageCount; i++) {
            dotCenterX[i] = (float) (startLeft + i * (dotDiameter + gap));
            LogUtil.loge("dotCenterX[" + i + "]==>" + dotCenterX[i]);
        }
        // todo just top aligning for now… should make this smarter
        dotTopY = top;
        dotCenterY = (float) (top + dotRadius);
        dotBottomY = top + dotDiameter;

        //设置当前页
        setCurrentPageImmediate();
    }

    private void setCurrentPageImmediate() {
        if (TextUtils.isNotEmpty(viewPager)) {
            currentPage = viewPager.getCurrentPage();
        } else {
            currentPage = 0;
        }
        if (TextUtils.isNotEmpty(dotCenterX) && dotCenterX.length > 0 && (TextUtils.isEmpty(moveAnimation) || !moveAnimation.isRunning())) {
            selectedDotX = dotCenterX[currentPage];
        }
    }

    private void resetState() {
        joiningFractions = new float[pageCount == 0 ? 0 : (pageCount - 1)];
        Arrays.fill(joiningFractions, 0f);
        dotRevealFractions = new float[pageCount];
        Arrays.fill(dotRevealFractions, 0f);
        retreatingJoinX1 = INVALID_FRACTION;
        retreatingJoinX2 = INVALID_FRACTION;
        selectedDotInPosition = true;
    }

    @Override
    public boolean onEstimateSize(int widthMeasureSpec, int heightMeasureSpec) {
        int desiredHeight = getDesiredHeight();
        int height;
        switch (Component.MeasureSpec.getMode(heightMeasureSpec)) {
            case MeasureSpec.PRECISE://EXACTLY
                height = Component.MeasureSpec.getSize(heightMeasureSpec);
                break;
            case Component.MeasureSpec.NOT_EXCEED://AT_MOST
                height = Math.min(desiredHeight, Component.MeasureSpec.getSize(heightMeasureSpec));
                break;
            default: // MeasureSpec.UNSPECIFIED
                height = desiredHeight;
                break;
        }

        int desiredWidth = getDesiredWidth();
        int width;
        switch (MeasureSpec.getMode(widthMeasureSpec)) {
            case MeasureSpec.PRECISE://EXACTLY
                width = MeasureSpec.getSize(widthMeasureSpec);
                break;
            case MeasureSpec.NOT_EXCEED://AT_MOST
                width = Math.min(desiredWidth, MeasureSpec.getSize(widthMeasureSpec));
                break;
            default: // MeasureSpec.UNSPECIFIED
                width = desiredWidth;
                break;
        }
        setEstimatedSize(width, height);
        calculateDotPositions(width, height);

        return false;
    }


    private int getDesiredHeight() {
        return getPaddingTop() + dotDiameter + getPaddingBottom();
    }

    private int getRequiredWidth() {
        return pageCount * dotDiameter + (pageCount - 1) * gap;
    }

    private int getDesiredWidth() {
        return getPaddingLeft() + getRequiredWidth() + getPaddingRight();
    }

    @Override
    public void addDrawTask(DrawTask task) {
        super.addDrawTask(task);
        task.onDraw(this, mCanvasForTaskOverContent);
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        if (viewPager == null || pageCount == 0) return;

        drawUnselected(canvas);

        drawSelected(canvas);

    }

    private void drawUnselected(Canvas canvas) {

        combinedUnselectedPath.rewind();
        LogUtil.loge("===========onDraw==============");
        // draw any settled, revealing or joining dots  画出任何固定的、暴露的或连接的点
        for (int page = 0; page < pageCount; page++) {
            int nextXIndex = page == pageCount - 1 ? page : page + 1;
            Path unselectedPath = getUnselectedPath(page,
                    dotCenterX[page],
                    dotCenterX[nextXIndex],
                    page == pageCount - 1 ? INVALID_FRACTION : joiningFractions[page],
                    dotRevealFractions[page]);
            unselectedPath.addPath(combinedUnselectedPath);
            combinedUnselectedPath.addPath(unselectedPath);
        }
        // draw any retreating joins  绘制后退连接
        if (retreatingJoinX1 != INVALID_FRACTION) {
            Path retreatingJoinPath = getRetreatingJoinPath();
            combinedUnselectedPath.addPath(retreatingJoinPath);
        }

        canvas.drawPath(combinedUnselectedPath, unselectedPaint);
    }

    /**
     * Unselected dots can be in 6 states:
     * <p>
     * #1 At rest
     * #2 Joining neighbour, still separate
     * #3 Joining neighbour, combined curved
     * #4 Joining neighbour, combined straight
     * #5 Join retreating
     * #6 Dot re-showing / revealing
     * <p>
     * It can also be in a combination of these states e.g. joining one neighbour while
     * retreating from another.  We therefore create a Path so that we can examine each
     * dot pair separately and later take the union for these cases.
     * <p>
     * This function returns a path for the given dot **and any action to it's right** e.g. joining
     * or retreating from it's neighbour
     *
     * @param page
     * @param centerX
     * @param nextCenterX
     * @param joiningFraction
     * @param dotRevealFraction
     * @return Path
     */
    private Path getUnselectedPath(int page,
                                   double centerX,
                                   double nextCenterX,
                                   double joiningFraction,
                                   double dotRevealFraction) {
        LogUtil.loge("=onPageSliding====drawUnselectedPath===page=" + page + "=centerX=" + centerX
                + "=nextCenterX=" + nextCenterX + "=joiningFraction=" + joiningFraction + "=dotRevealFraction=" + dotRevealFraction);
        unselectedDotPath.rewind();

        if ((joiningFraction == 0f || joiningFraction == INVALID_FRACTION)//浮点数运算问题
                && dotRevealFraction == 0f
                && !(page == currentPage && selectedDotInPosition == true)) {
            LogUtil.loge("==onPageSliding====drawUnselectedPath===dotCenterX[" + page + "]=" + dotCenterX[page] + "=dotCenterY=" + dotCenterY + "=dotRadius=" + dotRadius);

            // case #1 – At rest  画未选中圆
            unselectedDotPath.addCircle(dotCenterX[page], dotCenterY, (float) dotRadius, Path.Direction.CLOCK_WISE);//CW
        }

        LogUtil.loge("=onPageSliding===getUnselectedPath=====joiningFraction=" + joiningFraction + "=retreatingJoinX1=" + retreatingJoinX1 + "=INVALID_FRACTION=" + INVALID_FRACTION);
        //相吸动画
        if (joiningFraction > 0f && joiningFraction <= 0.5f
                && retreatingJoinX1 == INVALID_FRACTION) {// && !pageChanging
            // case #2 – Joining neighbour, still separate

            // start with the left dot
            unselectedDotLeftPath.rewind();

            // start at the bottom center  从底部中心开始
            unselectedDotLeftPath.moveTo((float) centerX, dotBottomY);

            // semi circle to the top center  半圆到上止点
            rectF.clear();
            rectF.fuse((float) (centerX - dotRadius), dotTopY, (float) (centerX + dotRadius), dotBottomY);
            unselectedDotLeftPath.arcTo(rectF, 90, 180, true);

            // cubic to the right middle  到右中间
            endX1 = (float) (centerX + dotRadius + (joiningFraction * gap));
            endY1 = dotCenterY;
            controlX1 = (float) (centerX + halfDotRadius);
            controlY1 = dotTopY;
            controlX2 = (float) endX1;
            controlY2 = (float) (endY1 - halfDotRadius);

            Point point1 = new Point(controlX1, controlY1);
            Point point2 = new Point(controlX2, controlY2);
            Point point3 = new Point((float) endX1, (float) endY1);
            unselectedDotLeftPath.cubicTo(point1, point2, point3);

            // cubic back to the bottom center   回到底部中心
            endX2 = (float) centerX;
            endY2 = dotBottomY;
            controlX1 = (float) endX1;
            controlY1 = (float) (endY1 + halfDotRadius);
            controlX2 = (float) (centerX + halfDotRadius);
            controlY2 = dotBottomY;

            Point point11 = new Point(controlX1, controlY1);
            Point point12 = new Point(controlX2, controlY2);
            Point point13 = new Point((float) endX2, (float) endY2);
            unselectedDotLeftPath.cubicTo(point11, point12, point13);

            unselectedDotPath.addPath(unselectedDotLeftPath);

            // now do the next dot to the right  现在做右边的下一个点
            unselectedDotRightPath.rewind();

            // start at the bottom center 从底部中心开始
            unselectedDotRightPath.moveTo((float) nextCenterX, dotBottomY);

            // semi circle to the top center  半圆到上止点
            rectF.clear();
            rectF.fuse((float) (nextCenterX - dotRadius), dotTopY, (float) (nextCenterX + dotRadius), dotBottomY);
            unselectedDotRightPath.arcTo(rectF, 90, -180, true);

            // cubic to the left middle   到左中间
            endX1 = (float) (nextCenterX - dotRadius - (joiningFraction * gap));
//            endY1 = dotCenterY;
            controlX1 = (float) (nextCenterX - halfDotRadius);
            controlY1 = dotTopY;
            controlX2 = (float) endX1;
            controlY2 = (float) (endY1 - halfDotRadius);

            Point point21 = new Point(controlX1, controlY1);
            Point point22 = new Point(controlX2, controlY2);
            Point point23 = new Point((float) endX1, (float) endY1);
            unselectedDotRightPath.cubicTo(point21, point22, point23);

            // cubic back to the bottom center  立方回到底部中心
            endX2 = (float) nextCenterX;
//            endY2 = dotBottomY;
            controlX1 = (float) endX1;
            controlY1 = (float) (endY1 + halfDotRadius);
            controlX2 = (float) (endX2 - halfDotRadius);
            controlY2 = dotBottomY;

            Point point31 = new Point(controlX1, controlY1);
            Point point32 = new Point(controlX2, controlY2);
            Point point33 = new Point((float) endX2, dotBottomY);
            unselectedDotRightPath.cubicTo(point31, point32, point33);
            unselectedDotPath.addPath(unselectedDotRightPath);

        }

        //相连之后的动画
        if (joiningFraction > 0.5f && joiningFraction < 1f
                && retreatingJoinX1 == INVALID_FRACTION) {// && pageChanging
            LogUtil.loge("===相连之后的动画====joiningFraction=" + joiningFraction + "=retreatingJoinX1=" + retreatingJoinX1);
            // case #3 – Joining neighbour, combined curved    案例3–连接相邻，组合曲线

            // adjust the fraction so that it goes from 0.3 -> 1 to produce a more realistic 'join'
            double adjustedFraction = (joiningFraction - 0.2f) * 1.25f;

            // start in the bottom left
            unselectedDotPath.moveTo((float) centerX, dotBottomY);

            // semi-circle to the top left  左上半圆
            rectF.clear();
            rectF.fuse((float) (centerX - dotRadius), dotTopY, (float) (centerX + dotRadius), dotBottomY);
            unselectedDotPath.arcTo(rectF, 90, 180, true);

            // bezier to the middle top of the join  贝塞尔到中间顶端的连接
            endX1 = centerX + dotRadius + (gap / (double) 2);
            endY1 = dotCenterY - (adjustedFraction * dotRadius);
            controlX1 = (float) (endX1 - (adjustedFraction * dotRadius));
            controlY1 = dotTopY;
            controlX2 = (float) (endX1 - ((1 - adjustedFraction) * dotRadius));
            controlY2 = (float) endY1;

            Point point14 = new Point(controlX1, controlY1);
            Point point24 = new Point(controlX2, controlY2);
            Point point34 = new Point((float) endX1, (float) endY1);
            unselectedDotPath.cubicTo(point14, point24, point34);

            // bezier to the top right of the join  连接右上角的bezier
            endX2 = (float) nextCenterX;
            endY2 = dotTopY;
            controlX1 = (float) (endX1 + ((1 - adjustedFraction) * dotRadius));
            controlY1 = (float) endY1;
            controlX2 = (float) (endX1 + (adjustedFraction * dotRadius));
            controlY2 = dotTopY;

            Point point15 = new Point(controlX1, controlY1);
            Point point25 = new Point(controlX2, controlY2);
            Point point35 = new Point((float) endX2, (float) endY2);

            unselectedDotPath.cubicTo(point15, point25, point35);

            // semi-circle to the bottom right  右下半圆
            rectF.clear();
            rectF.fuse((float) (nextCenterX - dotRadius), dotTopY, (float) (nextCenterX + dotRadius), dotBottomY);
            unselectedDotPath.arcTo(rectF, 270, 180, true);

            // bezier to the middle bottom of the join  贝塞尔到中间底部的连接
            // endX1 stays the same  endX1保持不变
            endY1 = dotCenterY + (adjustedFraction * (float) dotRadius);
            controlX1 = (float) (endX1 + (adjustedFraction * dotRadius));
            controlY1 = dotBottomY;
            controlX2 = (float) (endX1 + ((1 - adjustedFraction) * dotRadius));
            controlY2 = (float) endY1;

            Point point16 = new Point(controlX1, controlY1);
            Point point26 = new Point(controlX2, controlY2);
            Point point36 = new Point((float) endX1, (float) endY1);
            unselectedDotPath.cubicTo(point16, point26, point36);

            // bezier back to the start point in the bottom left  贝塞尔回到左下角的起点
            endX2 = (float) centerX;
            endY2 = dotBottomY;
            controlX1 = (float) (endX1 - ((1 - adjustedFraction) * dotRadius));
            controlY1 = (float) endY1;
            controlX2 = (float) (endX1 - (adjustedFraction * dotRadius));
            controlY2 = (float) endY2;

            Point point17 = new Point(controlX1, controlY1);
            Point point27 = new Point(controlX2, controlY2);
            Point point37 = new Point((float) endX2, (float) endY2);
            unselectedDotPath.cubicTo(point17, point27, point37);
        }

//        LogUtil.loge("=动画==joiningFraction="+joiningFraction+"=retreatingJoinX1="+retreatingJoinX1+"=changingpage="+pageChanging);

        if (joiningFraction == 1 && retreatingJoinX1 == INVALID_FRACTION) {
            // case #4 Joining neighbour, combined straight technically we could use case 3 for this
            // situation as well but assume that this is an optimization rather than faffing around
            // with beziers just to draw a rounded rect  用贝塞尔曲线画一个圆形的矩形  最后连接的矩形
            rectF.clear();
            rectF.fuse((float) (centerX - dotRadius), dotTopY, (float) (nextCenterX + dotRadius), dotBottomY);
            unselectedDotPath.addRoundRect(rectF, (float) dotRadius, (float) dotRadius, Path.Direction.CLOCK_WISE);//CW
        }

        // case #5 is handled by #getRetreatingJoinPath()
        // this is done separately so that we can have a single retreating path spanning  这是分开做的，这样我们就可以有一个单一的撤退路径
        // multiple dots and therefore animate it's movement smoothly  多个点，因此它的运动动画顺利

        if (dotRevealFraction > MINIMAL_REVEAL) {
            // case #6 – previously hidden dot revealing  以前隐藏的点显示动画
            unselectedDotPath.addCircle((float) centerX, dotCenterY, (float) dotRevealFraction * (float) dotRadius, Path.Direction.CLOCK_WISE);//CW
        }

        return unselectedDotPath;
    }

    //撤退路径
    private Path getRetreatingJoinPath() {
        unselectedDotPath.rewind();
        rectF.clear();
        rectF.fuse(retreatingJoinX1, dotTopY, retreatingJoinX2, dotBottomY);
        unselectedDotPath.addRoundRect(rectF, (float) dotRadius, (float) dotRadius, Path.Direction.CLOCK_WISE);//CW
        return unselectedDotPath;
    }

    private void drawSelected(Canvas canvas) {
        canvas.drawCircle((float) selectedDotX, dotCenterY, (float) dotRadius, selectedPaint);
    }

    /**
     * 选中页面后开始准备其他动画
     *
     * @param now int
     */
    private void setSelectedPage(int now) {
        if (now == currentPage || TextUtils.isEmpty(dotCenterX) || dotCenterX.length <= now) return;
        pageChanging = true;
        previousPage = currentPage;
        currentPage = now;
        int steps = Math.abs(now - previousPage);

        if (steps > 1) {
            if (now > previousPage) {
                for (int i = 0; i < steps; i++) {
                    setJoiningFraction(previousPage + i, (float) 1);
                }
            } else {
                for (int i = -1; i > -steps; i--) {
                    setJoiningFraction(previousPage + i, (float) 1);
                }
            }
        }
        //创建动画来移动选定的点-这个动画师将开始
        //当它移动75%的时候撤退。
        //撤退动画反过来会在
        //撤退已经过了任何需要揭露的点

        moveAnimation = createMoveSelectedAnimator(dotCenterX[now], previousPage, now, steps);
        moveAnimation.setCurveType(Animator.CurveType.LINEAR);//LINEAR
        moveAnimation.start();

    }

    //选中后创建移动动画
    private AnimatorValue createMoveSelectedAnimator(
            final double moveTo, int was, int now, int steps) {

//        LogUtil.loge("==createMoveSelectedAnimator==selectedDotX="+selectedDotX+"==moveTo="+moveTo+"==was="+ was+"=now="+ now+"=steps="+ steps);
        // create the actual move animator
//        AnimatorValue moveSelected = AnimatorValue.ofFloat(selectedDotX, moveTo);
        AnimatorValue moveSelected = new AnimatorValue();

//        LogUtil.loge("==createMoveSelectedAnimator==RightwardStartPredicate="+(moveTo - ((moveTo - selectedDotX) * 0.25f))+"==LeftwardStartPredicate="+(moveTo + ((selectedDotX - moveTo) * 0.25f)));
        // also set up a pending retreat anim – this starts when the move is 75% complete
        //等待撤退动画  超过75%就开始执行
//        LogUtil.loge("显示点触发阈值：RightwardStartPredicate="+(moveTo - ((moveTo - selectedDotX) * 0.25f)+"=LeftwardStartPredicate="+(moveTo + ((selectedDotX - moveTo) * 0.25f))));
        retreatAnimation = new PendingRetreatAnimator(was, now, steps, now > was ?
                new RightwardStartPredicate((float) (moveTo - ((moveTo - selectedDotX) *  0.25))) ://0.25f
                new LeftwardStartPredicate((float) (moveTo + ((selectedDotX - moveTo) *  0.25))));//0.25f

        retreatAnimation.setStateChangedListener(new Animator.StateChangedListener() {
            @Override
            public void onStart(Animator animator) {

            }

            @Override
            public void onStop(Animator animator) {

            }

            @Override
            public void onCancel(Animator animator) {

            }

            @Override
            public void onEnd(Animator animator) {
                resetState();
                pageChanging = false;

            }

            @Override
            public void onPause(Animator animator) {

            }

            @Override
            public void onResume(Animator animator) {

            }
        });

        moveSelected.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
            @Override
            public void onUpdate(AnimatorValue animatorValue, float v) {
                selectedDotX = selectedDotX + (moveTo - selectedDotX) * (double) v;
                //移动距离到达开始执行点的显示动画
                retreatAnimation.startIfNecessary((float) selectedDotX);
                invalidate();

            }
        });
        

        moveSelected.setStateChangedListener(new Animator.StateChangedListener() {
            @Override
            public void onStart(Animator animator) {
                selectedDotInPosition = false;
            }

            @Override
            public void onStop(Animator animator) {

            }

            @Override
            public void onCancel(Animator animator) {

            }

            @Override
            public void onEnd(Animator animator) {
                selectedDotInPosition = true;

            }

            @Override
            public void onPause(Animator animator) {

            }

            @Override
            public void onResume(Animator animator) {

            }
        });


        // slightly delay the start to give the joins a chance to run
        // unless dot isn't in position yet – then don't delay!
        moveSelected.setDelay(selectedDotInPosition ? animDuration / 4L : 0L);

        moveSelected.setDuration(animDuration * 3L / 4L);
        moveSelected.setCurveType(Animator.CurveType.LINEAR);//LINEAR

        return moveSelected;
    }

    /**
     * 设置每个点的动画百分比
     *
     * @param leftDot  leftDot
     * @param fraction leftDot
     */
    private void setJoiningFraction(int leftDot, float fraction) {
        if (leftDot < joiningFractions.length) {
//            if (leftDot == 1) {
//                //Log.d("PageIndicator", "dot 1 fraction:\t" + fraction);
//            }

            joiningFractions[leftDot] = fraction;
            invalidate();
        }
    }

    private void clearJoiningFractions() {
        Arrays.fill(joiningFractions, 0f);
        invalidate();
    }

    private void setDotRevealFraction(int dot, float fraction) {
        if (dot < dotRevealFractions.length) {
            dotRevealFractions[dot] = fraction;
        }
        invalidate();
    }

    private void cancelJoiningAnimations() {
        if (TextUtils.isNotEmpty(joiningAnimationSet) && joiningAnimationSet.isRunning()) {
            joiningAnimationSet.cancel();
        }
    }

    public abstract class PendingStartAnimator extends AnimatorValue {

        protected boolean hasStarted;
        protected StartPredicate predicate;

        public PendingStartAnimator(StartPredicate predicate) {
            super();
            this.predicate = predicate;
            hasStarted = false;
        }

        //设置为true就开始动画
        public void startIfNecessary(float currentValue) {
            //当前值是否大于初始值大于就开始执行动画
            if (!hasStarted && predicate.shouldStart(currentValue)) {
                start();
                hasStarted = true;
            }
        }
    }

    /**
     * An Animator that shows and then shrinks a retreating join between the previous and newly
     * 显示然后收缩上一个和新一个之间的后退连接的动画师
     * selected pages.  This also sets up some pending dot reveals – to be started when the retreat
     * 选定的页面。这也设置了一些待处理的点显示-在撤退时开始
     * has passed the dot to be revealed.
     */
    public class PendingRetreatAnimator extends PendingStartAnimator {

        public PendingRetreatAnimator(int was, int now, int steps, StartPredicate predicate) {
            super(predicate);
            setDuration(animHalfDuration);//setDurationInternal
            setCurveType(Animator.CurveType.LINEAR);//LINEAR

            // work out the start/end values of the retreating join from the direction we're
            // travelling in.  Also look at the current selected dot position, i.e. we're moving on
            // before a prior anim has finished.
            final float initialX1 = now > was ? (float) (Math.min(dotCenterX[was], selectedDotX) - dotRadius)
                    : (float) (dotCenterX[now] - dotRadius);

            final float finalX1 = (float) ((double) dotCenterX[now] - dotRadius);

            final float initialX2 = now > was ? (float) (dotCenterX[now] + dotRadius)
                    : (float) (Math.max(dotCenterX[was], selectedDotX) + dotRadius);

            final float finalX2 = (float) ((double) dotCenterX[now] + dotRadius);

//            LogUtil.loge("initialX1="+initialX1+"=finalX1="+finalX1+"==initialX2="+initialX2+"=finalX2="+finalX2+"==steps="+steps);
            //显示点动画
            revealAnimations = new PendingRevealAnimator[steps];
            final int[] dotsToHide = new int[steps];
            if (initialX1 != finalX1) { // rightward retreat
                /**
                 * 创建将在撤退经过时运行的显示动画
                 */
                for (int i = 0; i < steps; i++) {
                    revealAnimations[i] = new PendingRevealAnimator(was + i,
                            new RightwardStartPredicate(dotCenterX[was + i]));
                    dotsToHide[i] = was + i;
                }

                //setValueUpdateListener(AnimatorValue.ValueUpdateListener)
                setValueUpdateListener(new ValueUpdateListener() {
                    @Override
                    public void onUpdate(AnimatorValue valueAnimator, float v) {
                        retreatingJoinX1 = (float) ((double)initialX1 + ((double)finalX1 - (double)initialX1) * (double)v);
//                        LogUtil.loge("显示点动画==retreatingJoinX1="+retreatingJoinX1+"==v="+v);
                        invalidate();
                        // start any reveal animations if we've passed them
                        for (PendingRevealAnimator pendingReveal : revealAnimations) {
                            pendingReveal.startIfNecessary(retreatingJoinX1);
                        }

                    }
                });
            } else {
                for (int i = 0; i < steps; i++) {
                    revealAnimations[i] = new PendingRevealAnimator(was - i,
                            new LeftwardStartPredicate(dotCenterX[was - i]));
                    dotsToHide[i] = was - i;
                }
                setValueUpdateListener(new ValueUpdateListener() {
                    @Override
                    public void onUpdate(AnimatorValue valueAnimator, float v) {
                        // todo avoid autoboxing
//                        retreatingJoinX2 = (Float) valueAnimator.getAnimatedValue();

                        retreatingJoinX2 = (float) ((double)initialX2 + ((double)finalX2 - (double)initialX2) * (double) v);
                        invalidate();

                        // start any reveal animations if we've passed them
                        for (PendingRevealAnimator pendingReveal : revealAnimations) {
                            pendingReveal.startIfNecessary(retreatingJoinX2);
                        }

                    }
                });
            }

            setStateChangedListener(new StateChangedListener() {
                @Override
                public void onStart(Animator animator) {
                    cancelJoiningAnimations();
                    clearJoiningFractions();
                    // we need to set this so that the dots are hidden until the reveal anim runs
                    for (int dot : dotsToHide) {
                        setDotRevealFraction(dot, MINIMAL_REVEAL);
                    }
                    retreatingJoinX1 = initialX1;
                    retreatingJoinX2 = initialX2;
                    invalidate();
                }

                @Override
                public void onStop(Animator animator) {

                }

                @Override
                public void onCancel(Animator animator) {

                }

                @Override
                public void onEnd(Animator animator) {
                    retreatingJoinX1 = INVALID_FRACTION;
                    retreatingJoinX2 = INVALID_FRACTION;
                    invalidate();
                }

                @Override
                public void onPause(Animator animator) {

                }

                @Override
                public void onResume(Animator animator) {

                }
            });
        }
    }

    /**
     * An Animator that animates a given dot's revealFraction i.e. scales it up
     * 最后滑动完成前一个点由小到大显示动画  ok
     */
    public class PendingRevealAnimator extends PendingStartAnimator {

        private int dot;

        public PendingRevealAnimator(int dot, StartPredicate predicate) {
            super(predicate);

            this.dot = dot;
            setDuration(animHalfDuration);
            setCurveType(CurveType.LINEAR);//SMOOTH_STEP  LINEAR

            setValueUpdateListener(new ValueUpdateListener() {
                @Override
                public void onUpdate(AnimatorValue animatorValue, float v) {
                    //设置当前显示点的百分比
                    setDotRevealFraction(PendingRevealAnimator.this.dot, v);
                }
            });

            setStateChangedListener(new StateChangedListener() {
                @Override
                public void onStart(Animator animator) {

                }

                @Override
                public void onStop(Animator animator) {

                }

                @Override
                public void onCancel(Animator animator) {

                }

                @Override
                public void onEnd(Animator animator) {
//                    动画显示完初始化百分比
                    setDotRevealFraction(PendingRevealAnimator.this.dot, 0f);
                    invalidate();
                }

                @Override
                public void onPause(Animator animator) {

                }

                @Override
                public void onResume(Animator animator) {

                }
            });

        }
    }

    /**
     * A predicate used to start an animation when a test passes
     */
    public abstract class StartPredicate {

        protected float thresholdValue;

        public StartPredicate(float thresholdValue) {
            this.thresholdValue = thresholdValue;
        }

        abstract boolean shouldStart(float currentValue);

    }

    /**
     * A predicate used to start an animation when a given value is greater than a threshold
     */
    public class RightwardStartPredicate extends StartPredicate {

        public RightwardStartPredicate(float thresholdValue) {
            super(thresholdValue);
        }

        boolean shouldStart(float currentValue) {
            return currentValue > thresholdValue;
        }
    }

    /**
     * A predicate used to start an animation then a given value is less than a threshold
     */
    public class LeftwardStartPredicate extends StartPredicate {

        public LeftwardStartPredicate(float thresholdValue) {
            super(thresholdValue);
        }

        boolean shouldStart(float currentValue) {
            return currentValue < thresholdValue;
        }
    }

}
